Java Concurrency in Practice
نویسندگان
چکیده
QueuedSynchronizer, upon which most of the state dependent classes in java.util.concurrent are built(see Section 14.4), exploits the concept of exit protocol. Rather than letting synchronizer classes perform their ownnotification, it instead requires synchronizer methods to return a value indicating whether its action might haveunblocked one or more waiting threads. This explicit API requirement makes it harder to "forget" to notify on somestate transitions. 14.3. Explicit Condition Objects As we saw in Chapter 13, explicit Locks can be useful in some situations where intrinsic locks are too inflexible. Just asLock is a generalization of intrinsic locks, Condition (see Listing 14.10) is a generalization of intrinsic condition queues. Intrinsic condition queues have several drawbacks. Each intrinsic lock can have only one associated condition queue,which means that in classes like BoundedBuffer multiple threads might wait on the same condition queue for differentcondition predicates, and the most common pattern for locking involves exposing the condition queue object. Both ofthese factors make it impossible to enforce the uniform waiter requirement for using notifyAll. If you want to write aconcurrent object with multiple condition predicates, or you want to exercise more control over the visibility of thecondition queue, the explicit Lock and Condition classes offer a more flexible alternative to intrinsic locks andcondition queues. A Condition is associated with a single Lock, just as a condition queue is associated with a single intrinsic lock; to createa Condition, call Lock.newCondition on the associated lock. And just as Lock offers a richer feature set than intrinsiclocking, Condition offers a richer feature set than intrinsic condition queues: multiple wait sets per lock, interruptibleand uninterruptible condition waits, deadline based waiting, and a choice of fair or nonfair queueing. Listing 14.10. Condition Interface.public interface Condition {void await() throws InterruptedException;boolean await(long time, TimeUnit unit)throws InterruptedException;long awaitNanos(long nanosTimeout) throws InterruptedException;void awaitUninterruptibly();boolean awaitUntil(Date deadline) throws InterruptedException; void signal();void signalAll();} Unlike intrinsic condition queues, you can have as many Condition objects per Lock as you want. Condition objectsinherit the fairness setting of their associated Lock; for fair locks, threads are released from Condition.await in FIFOorder. Hazard warning: The equivalents of wait, notify, and notifyAll for Condition objects are await, signal, andsignalAll. However, Condition extends Object, which means that it also has wait and notify methods. Be sure touse the proper versions await and signal instead! Listing 14.11 shows yet another bounded buffer implementation, this time using two Conditions, notFull andnotEmpty, to represent explicitly the "not full" and "not empty" condition predicates. When take blocks because thebuffer is empty, it waits on notEmpty, and put unblocks any threads blocked in take by signaling on notEmpty. The behavior of ConditionBoundedBuffer is the same as BoundedBuffer, but its use of condition queues is morereadable it is easier to analyze a class that uses multiple Conditions than one that uses a single intrinsic conditionqueue with multiple condition predicates. By separating the two condition predicates into separate wait sets,Condition makes it easier to meet the requirements for single notification. Using the more efficient signal instead ofsignalAll reduces the number of context switches and lock acquisitions triggered by each buffer operation. Just as with built in locks and condition queues, the three way relationship among the lock, the condition predicate, andthe condition variable must also hold when using explicit Locks and Conditions. The variables involved in the conditionpredicate must be guarded by the Lock, and the Lock must be held when testing the condition predicate and whencalling await and signal. [11] ReentrantLock requires that the Lock be held when calling signal or signalAll, but Lock implementations are permitted toconstruct Conditions that do not have this requirement. 1897BPart IV: Advanced Topics 26BChapter 14 Building Custom Synchronizers Choose between using explicit Conditions and intrinsic condition queues in the same way as you would choosebetween ReentrantLock and synchronized: use Condition if you need its advanced features such as fair queueing ormultiple wait sets per lock, and otherwise prefer intrinsic condition queues. (If you already use ReentrantLock becauseyou need its advanced features, the choice is already made.) 14.4. Anatomy of a Synchronizer The interfaces of ReentrantLock and Semaphore have a lot in common. Both classes act as a "gate", allowing only alimited number of threads to pass at a time; threads arrive at the gate and are allowed through (lock or acquirereturns successfully), are made to wait (lock or acquire blocks), or are turned away (tryLock or tryAcquire returnsfalse, indicating that the lock or permit did not become available in the time allowed). Further, both allow interruptible,uninterruptible, and timed acquisition attempts, and both allow a choice of fair or nonfair queueing of waiting threads. Given this commonality, you might think that Semaphore was implemented on top of ReentrantLock, or perhapsReentrantLock was implemented as a Semaphore with one permit. This would be entirely practical; it is a commonexercise to prove that a counting semaphore can be implemented using a lock (as in SemaphoreOnLock in Listing 14.12)and that a lock can be implemented using a counting semaphore. In actuality, they are both implemented using a common base class, Abstract-QueuedSynchronizer (AQS)as are manyother synchronizers. AQS is a framework for building locks and synchronizers, and a surprisingly broad range ofsynchronizers can be built easily and efficiently using it. Not only are ReentrantLock and Semaphore built using AQS,but so are CountDownLatch, ReentrantReadWriteLock, SynchronousQueue, and FutureTask. [12] Java6 replaces the AQS based SynchronousQueue with a (more scalable) non blocking version. Listing 14.11. Bounded Buffer Using Explicit Condition Variables.@ThreadSafepublic class ConditionBoundedBuffer {protected final Lock lock = new ReentrantLock();// CONDITION PREDICATE: notFull (count < items.length)private final Condition notFull = lock.newCondition();// CONDITION PREDICATE: notEmpty (count > 0)private final Condition notEmpty = lock.newCondition();@GuardedBy("lock")private final T[] items = (T[]) new Object[BUFFER_SIZE];@GuardedBy("lock") private int tail, head, count; // BLOCKS-UNTIL: notFullpublic void put(T x) throws InterruptedException {lock.lock();try {while (count == items.length)notFull.await();items[tail] = x;if (++tail == items.length)tail = 0;++count;notEmpty.signal();} finally {lock.unlock();}} // BLOCKS-UNTIL: notEmptypublic T take() throws InterruptedException {lock.lock();try {while (count == 0)notEmpty.await();T x = items[head];items[head] = null;if (++head == items.length)head = 0;--count;notFull.signal();return x;} finally {lock.unlock();}}} 190 Java Concurrency In Practice Listing 14.12. Counting Semaphore Implemented Using Lock.// Not really how java.util.concurrent.Semaphore is implemented@ThreadSafepublic class SemaphoreOnLock {private final Lock lock = new ReentrantLock();// CONDITION PREDICATE: permitsAvailable (permits > 0)private final Condition permitsAvailable = lock.newCondition();@GuardedBy("lock") private int permits; SemaphoreOnLock(int initialPermits) {lock.lock();try {permits = initialPermits;} finally {lock.unlock();}} // BLOCKS-UNTIL: permitsAvailablepublic void acquire() throws InterruptedException {lock.lock();try {while (permits <= 0)permitsAvailable.await();--permits;} finally {lock.unlock();}} public void release() {lock.lock();try {++permits;permitsAvailable.signal();} finally {lock.unlock();}}} AQS handles many of the details of implementing a synchronizer, such as FIFO queuing of waiting threads. Individualsynchronizers can define flexible criteria for whether a thread should be allowed to pass or be required to wait. Using AQS to build synchronizers offers several benefits. Not only does it substantially reduce the implementationeffort, but you also needn't pay for multiple points of contention, as you would when constructing one synchronizer ontop of another. In SemaphoreOnLock, acquiring a permit has two places where it might block once at the lock guardingthe semaphore state, and then again if a permit is not available. Synchronizers built with AQS have only one point wherethey might block, reducing context switch overhead and improving throughput. AQS was designed for scalability, and allthe synchronizers in java.util.concurrent that are built with AQS benefit from this. 14.5. AbstractQueuedSynchronizer Most developers will probably never use AQS directly; the standard set of synchronizers covers a fairly wide range ofsituations. But seeing how the standard synchronizers are implemented can help clarify how they work. The basic operations that an AQS based synchronizer performs are some variants of acquire and release. Acquisition isthe state dependent operation and can always block. With a lock or semaphore, the meaning of acquire isstraightforward acquire the lock or a permit and the caller may have to wait until the synchronizer is in a state wherethat can happen. With CountDownLatch, acquire means "wait until the latch has reached its terminal state", and withFutureTask, it means "wait until the task has completed". Release is not a blocking operation; a release may allowthreads blocked in acquire to proceed.For a class to be state dependent, it must have some state. AQS takes on the task of managing some of the state for thesynchronizer class: it manages a single integer of state information that can be manipulated through the protectedgetState, setState, and compareAndSetState methods. This can be used to represent arbitrary state; for example,ReentrantLock uses it to represent the count of times the owning thread has acquired the lock, Semaphore uses it torepresent the number of permits remaining, and FutureTask uses it to represent the state of the task (not yet started,running, completed, cancelled). Synchronizers can also manage additional state variables themselves; for example,ReentrantLock keeps track of the current lock owner so it can distinguish between reentrant and contended lockacquisition requests. 1917BPart IV: Advanced Topics 26BChapter 14 Building Custom Synchronizers Acquisition and release in AQS take the forms shown in Listing 14.13. Depending on the synchronizer, acquisition mightbe exclusive, as with Reentrant-Lock, or nonexclusive, as with Semaphore and CountDownLatch. An acquire operationhas two parts. First, the synchronizer decides whether the current state permits acquisition; if so, the thread is allowedto proceed, and if not, the acquire blocks or fails. This decision is determined by the synchronizer semantics; forexample, acquiring a lock can succeed if the lock is unheld, and acquiring a latch can succeed if the latch is in its terminalstate. The second part involves possibly updating the synchronizer state; one thread acquiring the synchronizer can affectwhether other threads can acquire it. For example, acquiring a lock changes the lock state from "unheld" to "held", andacquiring a permit from a Semaphore reduces the number of permits left. On the other hand, the acquisition of a latchby one thread does not affect whether other threads can acquire it, so acquiring a latch does not change its state. Listing 14.13. Canonical Forms for Acquisition and Release in AQS.boolean acquire() throws InterruptedException {while (state does not permit acquire) {if (blocking acquisition requested) {enqueue current thread if not already queuedblock current thread}elsereturn failure}possibly update synchronization statedequeue thread if it was queuedreturn success} void release() {update synchronization stateif (new state may permit a blocked thread to acquire)unblock one or more queued threads} A synchronizer supporting exclusive acquisition should implement the protected methods TRyAcquire, TRyRelease,and isHeldExclusively, and those supporting shared acquisition should implement tryAcquireShared andTRyReleaseShared. The acquire, acquireShared, release, and releaseShared methods in AQS call the TRy forms ofthese methods in the synchronizer subclass to determine if the operation can proceed. The synchronizer subclass canuse getState, setState, and compareAndSetState to examine and update the state according to its acquire andrelease semantics, and informs the base class through the return status whether the attempt to acquire or release thesynchronizer was successful. For example, returning a negative value from TRyAcquireShared indicates acquisitionfailure; returning zero indicates the synchronizer was acquired exclusively; and returning a positive value indicates thesynchronizer was acquired nonexclusively. The TRyRelease and TRyReleaseShared methods should return true if therelease may have unblocked threads attempting to acquire the synchronizer. To simplify implementation of locks that support condition queues (like ReentrantLock), AQS also provides machineryfor constructing condition variables associated with synchronizers. 14.5.1. A Simple LatchOneShotLatch in Listing 14.14 is a binary latch implemented using AQS. It has two public methods, await and signal,that correspond to acquisition and release. Initially, the latch is closed; any thread calling await blocks until the latch isopened. Once the latch is opened by a call to signal, waiting threads are released and threads that subsequently arriveat the latch will be allowed to proceed. 192 Java Concurrency In Practice Listing 14.14. Binary Latch Using AbstractQueuedSynchronizer.@ThreadSafepublic class OneShotLatch {private final Sync sync = new Sync(); public void signal() { sync.releaseShared(0); } public void await() throws InterruptedException {sync.acquireSharedInterruptibly(0);} private class Sync extends AbstractQueuedSynchronizer {protected int tryAcquireShared(int ignored) {// Succeed if latch is open (state == 1), else failreturn (getState() == 1) ? 1 : -1;} protected boolean tryReleaseShared(int ignored) {setState(1); // Latch is now openreturn true; // Other threads may now be able to acquire
منابع مشابه
A method for verifying concurrent Java components based on an analysis of concurrency failures
The Java programming language supports concurrency. Concurrent programs are harder to verify than their sequential counterparts due to their inherent non-determinism and a number of specific concurrency problems, such as interference and deadlock. In previous work, we have developed the ConAn testing tool for the testing of concurrent Java components. ConAn has been found to be effective at tes...
متن کاملJava for parallel computing and as a general language for scientific and engineering simulation and modeling
We discuss the role of Java and Web technologies for general simulation. We classify the classes of concurrency typical in problems and analyze separately the role of Java in user interfaces, coarse grain software integration, and detailed computational kernels. We conclude that Java could become a major language for computational science, as it potentially o ers good performance, excellent use...
متن کاملLinda implementations in Java for concurrent systems
This paper surveys a number of the implementations of Linda that are available in Java. It provides some discussion of their strengths and weaknesses, and presents the results from benchmarking experiments using a network of commodity workstations. Some extensions to the original Linda programming model are also presented and discussed, together with examples of their application to parallel pr...
متن کاملJAC: declarative Java concurrency
The Java programming language has a low-level concurrency model which is hard to use and does not blend well with inheritance. JAC is an extension of Java that introduces a higher level of concurrency, hiding threads and separating thread synchronization from application logic in a declarative fashion. The emphasis is on limiting the differences between sequential and concurrent code, thus furt...
متن کاملImproving the official specification of Java bytecode verification
Bytecode verification is the main mechanism to ensure type safety in the Java Virtual Machine. Inadequacies in its official specification may lead to incorrect implementations where security can be broken and/or certain legal programs are rejected. This paper provides a comprehensive analysis of the specification, along with concrete suggestions for improvement. Copyright c © 2003 John Wiley & ...
متن کامل